﻿// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.CodeDom;
using System.CodeDom.Compiler;
using System.Collections;
using System.Collections.Specialized;

namespace System.ComponentModel.Design.Serialization;

/// <summary>
///  This class performs the same tasks as a CodeDomSerializer only serializing an object through this class defines a new type.
/// </summary>
[DefaultSerializationProvider(typeof(CodeDomSerializationProvider))]
public partial class TypeCodeDomSerializer : CodeDomSerializerBase
{
    // Used only during deserialization to provide name to object mapping.
    private HybridDictionary? _nameTable;
    private Dictionary<string, OrderedCodeStatementCollection>? _statementTable;
    private static readonly Attribute[] s_designTimeFilter = [DesignOnlyAttribute.Yes];
    private static readonly object s_initMethodKey = new();
    private static TypeCodeDomSerializer? s_default;

    internal static TypeCodeDomSerializer Default => s_default ??= new TypeCodeDomSerializer();

    /// <summary>
    ///  This method deserializes a previously serialized code type declaration. The default implementation performs
    ///  the following tasks:
    ///      • Case Sensitivity Checks: It looks for a CodeDomProvider service to decide if it should treat members
    ///          as case sensitive or case insensitive.
    ///      • Statement Sorting:  All member variables and local variables from init methods are stored in a table.
    ///          Then each statement in an init method is added to a statement collection grouped according
    ///          to its left hand side. So all statements assigning or operating on a particular variable are grouped
    ///          under that variable. Variables that have no statements are discarded.
    ///      • Deserialization: Finally, the statement collections for each variable are deserialized
    ///          according to the variable. Deserialize returns an instance of the root object.
    /// </summary>
    public virtual object Deserialize(IDesignerSerializationManager manager, CodeTypeDeclaration declaration)
    {
        ArgumentNullException.ThrowIfNull(manager);
        ArgumentNullException.ThrowIfNull(declaration);

        object rootObject;

        // Determine case-sensitivity
        bool caseInsensitive = false;
        CodeDomProvider? provider = manager.GetService<CodeDomProvider>();

        if (provider is not null)
        {
            caseInsensitive = ((provider.LanguageOptions & LanguageOptions.CaseInsensitive) != 0);
        }

        // Get and initialize the document type.
        Type? baseType = null;
        string baseTypeName = declaration.Name;

        foreach (CodeTypeReference typeRef in declaration.BaseTypes)
        {
            Type? t = manager.GetType(GetTypeNameFromCodeTypeReference(manager, typeRef));
            baseTypeName = typeRef.BaseType;
            if (t is not null && !(t.IsInterface))
            {
                baseType = t;
                break;
            }
        }

        if (baseType is null)
        {
            Error(manager, string.Format(SR.SerializerTypeNotFound, baseTypeName), SR.SerializerTypeNotFound);
        }

        if (GetReflectionTypeFromTypeHelper(manager, baseType).IsAbstract)
        {
            Error(manager, string.Format(SR.SerializerTypeAbstract, baseType.FullName), SR.SerializerTypeAbstract);
        }

        ResolveNameEventHandler onResolveName = new(OnResolveName);
        manager.ResolveName += onResolveName;
        rootObject = manager.CreateInstance(baseType, null, declaration.Name, true);

        // Now that we have the root object, we create a nametable and fill it with member declarations.
        int count = declaration.Members.Count;
        _nameTable = new HybridDictionary(count, caseInsensitive);
        _statementTable = new Dictionary<string, OrderedCodeStatementCollection>(count);
        Dictionary<string, string> names = new(count);
        RootContext rootCtx = new(new CodeThisReferenceExpression(), rootObject);
        manager.Context.Push(rootCtx);
        try
        {
            StringComparison compare = caseInsensitive ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal;
            foreach (CodeTypeMember typeMember in declaration.Members)
            {
                if (typeMember is CodeMemberField member)
                {
                    if (!string.Equals(member.Name, declaration.Name, compare))
                    {
                        // always skip members with the same name as the type -- because that's the name we use when we resolve "base" and "this" items...
                        _nameTable[member.Name] = member;

                        if (member.Type is not null && !string.IsNullOrEmpty(member.Type.BaseType))
                        {
                            names[member.Name] = GetTypeNameFromCodeTypeReference(manager, member.Type);
                        }
                    }
                }
            }

            CodeMemberMethod[] methods = GetInitializeMethods(manager, declaration)
                ?? throw new InvalidOperationException();

            // Walk through all of our methods and search for local variables. These guys get added to our nametable too.
            foreach (CodeMemberMethod method in methods)
            {
                foreach (CodeStatement statement in method.Statements)
                {
                    if (statement is CodeVariableDeclarationStatement local)
                    {
                        _nameTable[local.Name] = statement;
                    }
                }
            }

            // The name table should come pre-populated with our root expression.
            _nameTable[declaration.Name] = rootCtx.Expression;

            // We fill a "statement table" for everything in our init methods. This statement table is a dictionary whose
            // keys contain object names and whose values contain a statement collection of all statements with a LHS
            // resolving to an object by that name. If supportGenerate is true, FillStatementTable will skip methods
            // that are marked with the tag "GeneratedStatement".
            foreach (CodeMemberMethod method in methods)
            {
                FillStatementTable(manager, _statementTable, names, method.Statements, declaration.Name);
            }

            // Interesting problem. The CodeDom parser may auto generate statements that are associated with other methods.
            // VB does this, for example, to create statements automatically for Handles clauses. The problem with this
            // technique is that we will end up with statements that are related to variables that live solely in user code
            // and not in InitializeComponent. We will attempt to construct instances of these objects with limited success.
            // To guard against this, we check to see if the manager even supports this feature, and if it does, we must look
            // out for these statements while filling the statement collections.
            PropertyDescriptor? supportGenerate = manager.Properties["SupportsStatementGeneration"];
            if (supportGenerate is not null && supportGenerate.TryGetValue(manager, out bool supportGenerateValue) && supportGenerateValue)
            {
                // Ok, we must do the more expensive work of validating the statements we get.
                foreach (string name in _nameTable.Keys)
                {
                    if (!name.Equals(declaration.Name) && _statementTable.TryGetValue(name, out OrderedCodeStatementCollection? statements))
                    {
                        bool acceptStatement = false;
                        foreach (CodeStatement statement in statements)
                        {
                            object? genFlag = statement.UserData["GeneratedStatement"];
                            if (genFlag is not true)
                            {
                                acceptStatement = true;
                                break;
                            }
                        }

                        if (!acceptStatement)
                        {
                            _statementTable.Remove(name);
                        }
                    }
                }
            }

            // Design time properties must be resolved before runtime properties to make sure that properties like "language"
            // get established before we need to read values out the resource bundle.

            // Deserialize design time properties for the root component.
            DeserializePropertiesFromResources(manager, rootObject, s_designTimeFilter);
            // sort by the order so we deserialize in the same order the objects were declared in.
            OrderedCodeStatementCollection[] statementArray = new OrderedCodeStatementCollection[_statementTable.Count];
            _statementTable.Values.CopyTo(statementArray, 0);
            Array.Sort(statementArray, StatementOrderComparer.s_default);
            // make sure we have fully deserialized everything that is referenced in the statement table. Skip the root object for last
            OrderedCodeStatementCollection? rootStatements = null;
            foreach (OrderedCodeStatementCollection statements in statementArray)
            {
                if (statements.Name.Equals(declaration.Name))
                {
                    rootStatements = statements;
                }
                else
                {
                    DeserializeName(manager, statements.Name, statements);
                }
            }

            if (rootStatements is not null)
            {
                DeserializeName(manager, rootStatements.Name, rootStatements);
            }
        }
        finally
        {
            _nameTable = null;
            _statementTable = null;
            Debug.Assert(manager.Context.Current == rootCtx, "Context stack corrupted");
            manager.ResolveName -= onResolveName;
            manager.Context.Pop();
        }

        return rootObject;
    }

    /// <summary>
    ///  This takes the given name and deserializes it from our name table. Before blindly deserializing it checks
    ///  the contents of the name table to see if the object already exists within it. We do this because
    ///  deserializing one object may call back into us through OnResolveName and deserialize another.
    /// </summary>
    private object? DeserializeName(IDesignerSerializationManager manager, string name, CodeStatementCollection? statements)
    {
        object? value;

        // If the name we're looking for isn't in our dictionary, we return null.
        // It is up to the caller to decide if this is an error or not.
        value = _nameTable![name];
        CodeObject? codeObject = value as CodeObject;
        string? typeName = null;
        CodeMemberField? field = null;

        if (codeObject is not null)
        {
            // If we fail, don't return a CodeDom element to the caller! Also clear out our nametable entry here.
            // A badly written serializer may cause a recursion here, and we want to stop it.
            value = null;
            _nameTable[name] = null;

            // What kind of code object is this?
            if (codeObject is CodeVariableDeclarationStatement declaration)
            {
                typeName = GetTypeNameFromCodeTypeReference(manager, declaration.Type);
            }
            else
            {
                field = codeObject as CodeMemberField;
                if (field is not null)
                {
                    typeName = GetTypeNameFromCodeTypeReference(manager, field.Type);
                }
                else if (manager.TryGetContext(out RootContext? rootCtx) && codeObject is CodeExpression exp && rootCtx.Expression == exp)
                {
                    value = rootCtx.Value;
                    typeName = TypeDescriptor.GetClassName(value);
                }
                else
                {
                    Debug.Fail("Unrecognized code object in nametable.");
                }
            }
        }
        else if (value is null)
        {
            // See if the container has this object. This may be necessary for visual inheritance.
            IContainer? container = manager.GetService<IContainer>();
            if (container is not null)
            {
                IComponent? comp = container.Components[name];
                if (comp is not null)
                {
                    typeName = comp.GetType().FullName;

                    // We had to go to the host here, so there isn't a nametable entry here -- push in the component
                    // here so we don't accidentally recurse when we try to deserialize this object.
                    _nameTable[name] = comp;
                }
            }
        }

        if (typeName is not null)
        {
            // Default case -- something that needs to be deserialized
            Type? type = manager.GetType(typeName);
            if (type is null)
            {
                manager.ReportError(new CodeDomSerializerException(string.Format(SR.SerializerTypeNotFound, typeName), manager));
            }
            else
            {
                if (statements is null && _statementTable!.TryGetValue(name, out OrderedCodeStatementCollection? statementOut))
                {
                    statements = statementOut;
                }

                if (statements is not null && statements.Count > 0)
                {
                    CodeDomSerializer? serializer = GetSerializer(manager, type);
                    if (serializer is null)
                    {
                        // We report this as an error. This indicates that there are code statements in
                        // initialize component that we do not know how to load.
                        manager.ReportError(new CodeDomSerializerException(string.Format(SR.SerializerNoSerializerForComponent, type.FullName), manager));
                    }
                    else
                    {
                        try
                        {
                            value = serializer.Deserialize(manager, statements);
                            // Search for a modifiers property, and set it.
                            if (value is not null && field is not null)
                            {
                                PropertyDescriptor? prop = TypeDescriptor.GetProperties(value)["Modifiers"];

                                if (prop is not null && prop.PropertyType == typeof(MemberAttributes))
                                {
                                    MemberAttributes modifiers = field.Attributes & MemberAttributes.AccessMask;

                                    prop.SetValue(value, modifiers);
                                }
                            }

                            _nameTable[name] = value;
                        }
                        catch (Exception ex)
                        {
                            manager.ReportError(ex);
                        }
                    }
                }
            }
        }

        return value;
    }

    /// <summary>
    ///  This method returns the method to emit all of the initialization code to for the given member.
    ///  The default implementation returns an empty constructor.
    /// </summary>
    protected virtual CodeMemberMethod GetInitializeMethod(IDesignerSerializationManager manager, CodeTypeDeclaration declaration, object value)
    {
        ArgumentNullException.ThrowIfNull(manager);
        ArgumentNullException.ThrowIfNull(declaration);
        ArgumentNullException.ThrowIfNull(value);

        if (declaration.UserData[s_initMethodKey] is not CodeConstructor ctor)
        {
            ctor = new CodeConstructor();
            declaration.UserData[s_initMethodKey] = ctor;
        }

        return ctor;
    }

    /// <summary>
    ///  This method returns an array of methods that need to be interpreted during deserialization.
    ///  The default implementation returns a single element array with the constructor in it.
    /// </summary>
    protected virtual CodeMemberMethod[] GetInitializeMethods(IDesignerSerializationManager manager, CodeTypeDeclaration declaration)
    {
        ArgumentNullException.ThrowIfNull(manager);
        ArgumentNullException.ThrowIfNull(declaration);

        foreach (CodeTypeMember member in declaration.Members)
        {
            if (member is CodeConstructor ctor && ctor.Parameters.Count == 0)
            {
                return [ctor];
            }
        }

        return [];
    }

    /// <summary>
    ///  Called by the serialization manager to resolve a name to an object.
    /// </summary>
    private void OnResolveName(object? sender, ResolveNameEventArgs e)
    {
        Debug.Assert(_nameTable is not null, "OnResolveName called and we are not deserializing!");

        // If someone else already found a value, who are we to complain?
        if (e.Value is null)
        {
            IDesignerSerializationManager manager = (IDesignerSerializationManager)sender!;
            e.Value = DeserializeName(manager, e.Name!, null);
        }
    }

    /// <summary>
    ///  This method serializes the given root object and optional collection of members to create a new type definition.
    ///  The members collection can be null or empty. If it contains values, these values will be serialized.
    ///  Values themselves may decide to serialize as either member variables or local variables.
    ///  This determination is done by looking for an extender property on the object called GenerateMember.
    ///  If <see langword="true"/>, a member is generated. Otherwise, a local variable is generated.
    ///  For convenience, the members collection can contain the root object.
    ///  In this case the root object will not also be added as a member or local variable.
    ///  The return value is a CodeTypeDeclaration that defines the root object.
    ///  The name of the type will be taken from the root object’s name, if it was a named object.
    ///  If not, a name will be fabricated from the simple type name of the root class.
    ///  The default implementation of Serialize performs the following tasks:
    ///  •    Context Seeding. The serialization context will be “seeded” with data including the RootContext,
    ///           and CodeTypeDeclaration.
    ///  •    Member Serialization. Next Serialize will walk all of the members and call SerializeToExpression.
    ///           Because serialization is done opportunistically in SerializeToExpression,
    ///           this ensures that we do not serialize twice.
    ///  •    Root Serialization. Finally, the root object is serialized and its statements are added to the
    ///           statement collection.
    ///  •    Statement Integration. After all objects have been serialized the Serialize method orders the statements
    ///           and adds them to a method returned from GetInitializeMethod. Finally, a constructor is fabricated
    ///           that calls all of the methods returned from GetInitializeMethod (this step is skipped for cases when
    ///           GetInitializeMethod returns a constructor.
    /// </summary>
    public virtual CodeTypeDeclaration Serialize(IDesignerSerializationManager manager, object root, ICollection? members)
    {
        ArgumentNullException.ThrowIfNull(manager);
        ArgumentNullException.ThrowIfNull(root);

        // As a type serializer we are responsible for creating the type declaration. Other serializers may access this
        // type declaration and add members to it, so we need to place it on the context stack. The serialization process
        // also looks at the root context to see if there is a root component. The root context is also used by the serializers
        // to add statement collections for serialized components.

        CodeTypeDeclaration docType = new(manager.GetName(root)!);
        CodeThisReferenceExpression thisRef = new();
        RootContext rootCtx = new(thisRef, root);
        StatementContext statementCtx = new();
        // Populate the statement context with a list of members we'd like to see statements for
        statementCtx.StatementCollection.Populate(root);
        if (members is not null)
        {
            statementCtx.StatementCollection.Populate(members);
        }

        docType.BaseTypes.Add(root.GetType());
        manager.Context.Push(docType);
        manager.Context.Push(rootCtx);
        manager.Context.Push(statementCtx);
        try
        {
            // Do each component, skipping us, since we handle our own serialization.
            // This looks really sweet, but is it worth it?  We take the perf hit of a quicksort + the allocation
            // overhead of 4 bytes for each component. Profiles show this as a 2% cost for a form with 100 controls.
            // Let's meet the perf goals first, then consider uncommenting this.
            if (members is not null)
            {
                foreach (object member in members)
                {
                    if (member != root)
                    {
                        // This returns an expression for the object, if possible. We ignore that. Besides returning an
                        // expression, it fills the statement table in the statement context and we're very interested
                        // in that. After serializing everything we will walk over the statement context's statement
                        // table. We will validate that each and every member we've serialized has a presence in the
                        // statement table. If it doesn't, that's an error in the member's serializer.
                        SerializeToExpression(manager, member);
                    }
                }
            }

            // Now, do the root object last.
            SerializeToExpression(manager, root);

            // After serializing everything we will walk over the statement context's statement table. We will validate
            // that each and every member we've serialized has a presence in the statement table. If it doesn't, that's
            // an error in the member's serializer.
            IntegrateStatements(manager, root, members, statementCtx, docType);
        }
        finally
        {
            Debug.Assert(manager.Context.Current == statementCtx, "Somebody messed up our context stack");
            manager.Context.Pop();
            manager.Context.Pop();
            manager.Context.Pop();
        }

        return docType;
    }

    /// <summary>
    ///  Takes the statement context and integrates all the statements into the correct methods.
    ///  Then, those methods are added to the code type declaration.
    /// </summary>
    private void IntegrateStatements(IDesignerSerializationManager manager, object root, ICollection? members, StatementContext statementCtx, CodeTypeDeclaration typeDecl)
    {
        Dictionary<string, CodeMethodMap> methodMap = [];

        // Go through all of our members and root object and fish out matching statement context info for each object.
        // The statement context will probably contain more objects than our members, because each object that returned
        // a statement collection was placed in the context. That's fine, because for each major component we serialized
        // it pushed its statement collection on the context stack and statements were added there as well, forming a
        // complete graph.

        if (members is not null)
        {
            foreach (object member in members)
            {
                if (member != root)
                { // always skip the root and do it last
                    CodeStatementCollection? statements = statementCtx.StatementCollection[member];
                    if (statements is not null)
                    {
                        CodeMemberMethod method = GetInitializeMethod(manager, typeDecl, member)
                            ?? throw new InvalidOperationException();

                        if (!methodMap.TryGetValue(method.Name, out CodeMethodMap? map))
                        {
                            map = new CodeMethodMap(method);
                            methodMap[method.Name] = map;
                        }

                        if (statements.Count > 0)
                        {
                            map.Add(statements);
                        }
                    }
                }
            }
        }

        // Finally, do the same thing for the root object.
        CodeStatementCollection? rootStatements = statementCtx.StatementCollection[root];
        if (rootStatements is not null)
        {
            CodeMemberMethod rootMethod = GetInitializeMethod(manager, typeDecl, root)
                ?? throw new InvalidOperationException();

            if (!methodMap.TryGetValue(rootMethod.Name, out CodeMethodMap? rootMap))
            {
                rootMap = new CodeMethodMap(rootMethod);
                methodMap[rootMethod.Name] = rootMap;
            }

            if (rootStatements.Count > 0)
            {
                rootMap.Add(rootStatements);
            }
        }

        // Final step -- walk through all of the sections and emit them to the type declaration.
        foreach (CodeMethodMap map in methodMap.Values)
        {
            map.Combine();
            typeDecl.Members.Add(map.Method);
        }
    }
}
